# import libraries
import pygame
import math
import random
import os
from button import Button
from crosshair import Crosshair
from imageutils import scale_image
from imageutils import get_imagelist_from_horizontal_sheet

# 初始化pygame
pygame.init()

# 设置和显示游戏窗体(用全大写字母+下划线表示参数常量，下同)
SCREEN_WIDTH = 1136
SCREEN_HEIGHT = 640
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption('Hold the Fort by Snow')
pygame.display.set_icon(pygame.image.load('img/myimg/snowball.png'))
clock = pygame.time.Clock()

# 定义游戏中所需的常量
FRAMES_PER_SECOND = 60
# 游戏所需常量：初始位置（无特殊说明时，坐标值均为左上角的值）
CASTLE_INITIAL_X = 220
CASTLE_INITIAL_Y = SCREEN_HEIGHT - 250
DEFENDER_INITIAL_Y = CASTLE_INITIAL_Y + 200
ZOMBIE_INITIAL_X = SCREEN_WIDTH - 100
ZOMBIE_INITIAL_Y = (CASTLE_INITIAL_Y - 50, CASTLE_INITIAL_Y + 50)
SNOWBALL_INITIAL_X = 280
SNOWBALL_INITIAL_Y = 430
# 游戏所需常量：初始属性
CASTLE_INITIAL_HEALTH = 1000
ZOMBIE_INITIAL_HEALTH = 20
CASTLE_INITIAL_MONEY = 1000
ZOMBIE_INITIAL_SPEED = 1
SNOWBALL_INITIAL_SPEED = 0.6
SNOWBALL_INITIAL_POWER = 30
SNOWBOMB_INITIAL_POWER = 40
ZOMBIE_INITIAL_POWER = 50
# 游戏所需常量：冷却时间
NEW_ZOMBIE_COOLDOWN = 1000
# 游戏所需常量：花费金钱
DEFENDER_COST = 1800
REPAIR_COST = 800
INCREASELIFE_COST = 800
SNOWBOMB_COST = 300

# 其他常量
LEVEL_TOTAL_ENEMY_GETTER = 3
MAX_DEFENDER = 11
DEFENDER_POSITIONS = [
    [50, DEFENDER_INITIAL_Y],
    [150, DEFENDER_INITIAL_Y],
    [250, DEFENDER_INITIAL_Y],
    [350, DEFENDER_INITIAL_Y],
    [450, DEFENDER_INITIAL_Y],
    [550, DEFENDER_INITIAL_Y],
    [650, DEFENDER_INITIAL_Y],
    [750, DEFENDER_INITIAL_Y],
    [850, DEFENDER_INITIAL_Y],
    [950, DEFENDER_INITIAL_Y],
    [1050, DEFENDER_INITIAL_Y]
]

# 定义游戏中所需的变量
# 关卡相关变量
current_level = 1
level_current_enemy = 0
level_total_enemy = current_level * LEVEL_TOTAL_ENEMY_GETTER
level_current_ufo = 0
next_level = False
game_over = False
# 其他变量
highest_score = 0
last_zombie_time = pygame.time.get_ticks()
enemies_living = 0

# 图片载入
# 图片载入：背景图片
bg = pygame.image.load('img/myimg/bg2_snow.png').convert_alpha()
# 图片载入：城堡
castle_image = pygame.image.load('img/myimg/mycastle1.png').convert_alpha()
castle_image = scale_image(castle_image, 1.1, 1.1)
# 图片载入：雪球（武器）
snowball_image = pygame.image.load('img/myimg/snowball.png')
snowball_image = scale_image(snowball_image, 0.8, 0.8)
# 图片载入：雪弹（武器）
snowbomb_image = pygame.image.load('img/snowbomb.png')
snowbomb_image = scale_image(snowbomb_image, 1.2, 1.2)
# 图片载入：雪弹（武器）
grenade_image = pygame.image.load('img/grenade.png')
grenade_image = scale_image(grenade_image, 0.3, 0.3)
# 图片载入：防御塔
defender_image = pygame.image.load('img/myimg/defender1.png').convert_alpha()
defender_image = scale_image(defender_image, 0.1, 0.1)
# 图片载入：三种按钮
defender_buttion_image = scale_image(defender_image, 1.5, 1)
repair_button_image = scale_image(pygame.image.load('img/button/repair.png').convert_alpha(), 1.5, 1.5)
maxhealth_button_image = pygame.image.load('img/button/maxhealth.png').convert_alpha()
# 图片载入：雪弹击中
bombed_images = get_imagelist_from_horizontal_sheet('img/jizhong.png', 6, 2, (0, 0, 0))
# 图片载入：准星
crosshair_image = pygame.image.load('img/crosshair.png').convert_alpha()  #
crosshair_image = scale_image(crosshair_image, 0.025, 0.025)

# shoot函数，用于捕获鼠标事件，让城堡发射弹药。鼠标左键发射雪球，鼠标右键发射雪弹。
is_shooting_left = False;  # 鼠标左键子弹标志位，可以防止连发
is_shooting_right = False;  # 鼠标右键子弹标志位，可以防止连发


def shoot(shoot_initial_pos: tuple, shoot_speed: int):
    global is_shooting_right
    global is_shooting_left
    mouse_pos = pygame.mouse.get_pos()  # 捕获鼠标位置
    x_dist = mouse_pos[0] - shoot_initial_pos[0]  # 水平距离
    y_dist = -(mouse_pos[1] - shoot_initial_pos[1])  # 数值聚聚
    shoot_angle = math.degrees(math.atan2(y_dist, x_dist))  # 发射角度（角度制）

    if pygame.mouse.get_pressed()[0] and is_shooting_left == False and mouse_pos[1] > 80:  # 此时应当发射新雪球
        is_shooting_left = True  # 左标志位设为True，可以防止连发
        snowball = Snowball(snowball_image, (shoot_initial_pos[0], shoot_initial_pos[1]), shoot_speed, shoot_angle,
                            SNOWBALL_INITIAL_POWER)  # 创建新的雪球对象
        snowball_group.add(snowball)  # 把雪球对象加入到雪球组中去

    if pygame.mouse.get_pressed()[0] == False:  # 当释放鼠标左键
        is_shooting_left = False  # 标志位改回False

    if pygame.mouse.get_pressed()[2] and castle.money >= SNOWBOMB_COST and is_shooting_right == False and mouse_pos[
        1] > 80:  # 此时应当发射新雪弹
        is_shooting_right = True  # 右标志位设为True，可以防止连发
        snowbomb = Snowbomb(snowbomb_image, (shoot_initial_pos[0], shoot_initial_pos[1]), shoot_speed, shoot_angle,
                            SNOWBOMB_INITIAL_POWER, mouse_pos, bombed_images, castle)  # 创建新的雪弹对象
        snowbomb_group.add(snowbomb)  # 把雪弹对象加入到雪球组中去

    if pygame.mouse.get_pressed()[2] == False:  # 当释放鼠标右键
        is_shooting_right = False  # 标志位改回False


# 该函数检测雪球是否击中了某个敌人
def check_snowball_zombie(snowball_group: pygame.sprite.Group, enemy_group: pygame.sprite.Group) -> None:
    for snowball in snowball_group:  # 遍历雪球组中的每一个雪球
        for enemy in enemy_group:  # 遍历敌人组中的每一个敌人
            if enemy.rect.colliderect(snowball.rect):  # 如果击中了
                snowball.kill()  # 回收雪球，该雪球不能再攻击其他敌人
                enemy.health -= snowball.power  # 敌人的健康值 -= 雪球的攻击力
                break  # 一颗雪球只能击中一个僵尸，所以只要进入if，就代表击中了一个敌人，该雪球就不需要遍历其他敌人了


# 该函数检测UFO的子弹是否击中了城堡
def check_grenade_castle(grenade_group: pygame.sprite.Group, castle) -> None:
    for grenade in grenade_group:  # 遍历子弹组中的每一个子弹
        if grenade.rect.colliderect(castle.rect):  # 如果击中了
            grenade.kill()  # 回收子弹，该子弹不能再重复攻击城堡
            castle.health -= grenade.power  # 城堡的健康值 -= 子弹的攻击力


# 从文件中读取最高成绩
if os.path.exists('score.txt'):  # 如果系统中存在该路径
    with open('score.txt', 'r') as file:  # 以只读形式打开存放最高成绩的文件
        highest_score = int(file.read())  # 读出最高成绩

# 用RBG三元组定义之后用到的颜色常量
WHITE = (255, 255, 255)
YELLOW = (255, 255, 0)
GREY = (100, 100, 100)
LIGHTBLUE = (135, 206, 235)
RED = (255, 0, 0)

# 定义字体
font_1 = pygame.font.SysFont('Futura', 30)
font_2 = pygame.font.SysFont('Futura', 60)

# 定义敌人种类及其图集（用于模拟动画效果）
zombie_all_images = []  # 僵尸的总图集，里面包含的是一个个不同种类僵尸的子图列表
zombie_types = ['NormalZombie', 'FlagZombie', 'BucketheadZombie']
zombie_health = 100

zombie_states = ['normal', 'attacking', 'die']  # 僵尸的状态种类
image_num_of_per_state_per_zombie = [22, 21, 10, 12, 11, 17, 15, 11, 17]  # 每个僵尸的每个状态所包含的图片的个数
index_image_num_of_per_state_per_zombie = 0
for zombie in zombie_types:
    zombie_onetype_list = []  # 这是某一种僵尸的图集，里面包含的是该僵尸不同状态的列表
    for state in zombie_states:
        zombie_onetype_onestate_list = []  # 这是某一种僵尸的某一个状态的图集，里面抱哈难度是一个个的图片
        for i in range(image_num_of_per_state_per_zombie[index_image_num_of_per_state_per_zombie]):
            zombie_image = pygame.image.load(f'img/zombies/{zombie}/{state}/{i}.png').convert_alpha()  # 从计算机文件系统里读取图片
            zombie_width = zombie_image.get_width()  # 获取僵尸的宽度
            zombie_height = zombie_image.get_height()  # 获取僵尸的高度
            zombie_image = scale_image(zombie_image, 0.5, 0.5)  # 对图片进行缩放
            zombie_onetype_onestate_list.append(zombie_image)  # 把当前图片加入到列表中
        index_image_num_of_per_state_per_zombie += 1
        zombie_onetype_list.append(zombie_onetype_onestate_list)  # 把当前这种僵尸的当前状态图集加入到当前这种僵尸的总图集中去
    zombie_all_images.append(zombie_onetype_list)  # 把当前这种僵尸的图集加入到所有僵尸总图集中去

# 定义敌人种类及其图集（用于模拟动画效果）
ufo_image_count = [1, 17, 17]  # UFO每种状态所含有的图片帧数
ufo_health = 250  # ufo的初始健康值
ufo_state_list = ['normal', 'dieToLeft', 'dieToRight']  # ufo的状态列表
ufo_all_images = []  # ufo的总图集，里面包含三个子图集，即ufo每种状态的图集
for _ in range(len(ufo_image_count)):  # 遍历ufo的三种状态，从而从计算机文件系统里读取读取三种状态的图集
    ufo_onestate_list = []  # ufo每种状态的图集
    for i in range(ufo_image_count[_]):
        ufo_image = pygame.image.load(f'img/ufo/{ufo_state_list[_]}/{i}.png').convert_alpha()  # 从计算机文件系统里读取图片
        ufo_width = ufo_image.get_width()  # 获取ufo的宽度
        ufo_height = ufo_image.get_height()  # 获取ufo的高度
        ufo_image = scale_image(ufo_image, 1, 1)  # 对图片进行缩放
        ufo_onestate_list.append(ufo_image)  # 把当前的图片加入到当前的ufo某个状态的图集中去
    ufo_all_images.append(ufo_onestate_list)  # 把ufo三种状态的子图集全部加入到ufo_all_images图集中去


# 把内容为text_content的文字以text_font字体以text_color颜色显示到text_pos位置上去
def draw_text(text_content, text_font, text_color, text_pos):
    text_surface = text_font.render(text_content, True, text_color)
    screen.blit(text_surface, text_pos)


# 在屏幕上显示各种文字信息
def display_textinfo() -> None:
    draw_text('Health: ' + str(castle.health) + " / " + str(castle.max_health), font_1, RED, (10, 200))
    draw_text('Level: ' + str(current_level), font_1, YELLOW, (10, 10))
    draw_text('Score: ' + str(castle.score), font_1, YELLOW, (210, 10))
    draw_text('Highest Score: ' + str(highest_score), font_1, YELLOW, (410, 10))
    draw_text('Money: ' + str(castle.money), font_1, YELLOW, (710, 10))

    draw_text(str(REPAIR_COST), font_1, GREY, (SCREEN_WIDTH - 250, 70))
    draw_text(str(DEFENDER_COST), font_1, GREY, (SCREEN_WIDTH - 180, 70))
    draw_text(str(INCREASELIFE_COST), font_1, GREY, (SCREEN_WIDTH - 95, 70))


# 城堡类
class Castle():
    def __init__(self, image: pygame.image, initial_pos: tuple, initial_health: int, initial_money: int):
        '''
        # 城堡类：
        :param image: 图片
        :param initial_pos: (初始横坐标、初始纵坐标)
        :param initial_health: 初始健康值
        :param initial_money: 初始金钱数
        '''

        self.image = image  # 初始化城堡图片

        # 获取rect，rect比城堡的区域要小一些，当敌人攻击到rect区域时，城堡会减少健康值。rect比城堡小的原因因为我选取的城堡图片为菱形，外围大多是空白区域。
        self.rect = pygame.Rect(0, 0, self.image.get_width() - 130, self.image.get_height() - 130)
        self.rect.center = initial_pos

        self.health = initial_health  # 城堡的健康值
        self.max_health = self.health  # 城堡的最大健康值
        self.money = initial_money  # 城堡的金钱值

        self.fired = False  # 城堡当前是否处于开火状态
        self.score = 0  # 当前的成绩

        # 血条的相关属性
        self.fullbloodrect = pygame.Rect(10, 180, 427, 12)  # 满血血条的rect
        self.fullbloodimage = scale_image(pygame.image.load('img/bloodline/hese.jfif'), 0.7, 0.02)  # 满血血条的图像
        self.currentbloodimage = scale_image(pygame.image.load('img/bloodline/yello.webp'), 1, 0.04)  # 当前血条的图像

    # 在屏幕上画出当前城堡及其相关元素，所以传入了一个屏幕对象
    def draw(self, screen: pygame.Surface) -> None:
        # 把城堡的图像画在屏幕上
        screen.blit(self.image, (self.rect.x - 70, self.rect.y - 70))

        # 把城堡的血条画在屏幕行
        if self.health > 0:  # 如果当前生命值小于等于0，既画满血的后景血条，又画实际的前景血条
            screen.blit(self.fullbloodimage, self.fullbloodrect)  # 画后景血条
            self.scale_x = (self.fullbloodimage.get_width() * (self.health / self.max_health)) / (
                self.currentbloodimage.get_width())  # 计算前景血条的宽度比例
            screen.blit(scale_image(self.currentbloodimage, self.scale_x, 1), self.fullbloodrect)  # 画前景血条
            pygame.draw.rect(screen, (0, 0, 1), self.fullbloodrect, 1)  # 画出血条的黑框
        else:  # 如果当前生命值小于等于0，只画满血的后景血条
            screen.blit(self.fullbloodimage, self.fullbloodrect)  # 画满血的后景血条


# 防御塔类
class Defender(pygame.sprite.Sprite):
    def __init__(self, image: pygame.image, pos: tuple):
        '''
        :param image: 防御塔的图片
        :param pos: 防御塔的位置
        '''
        pygame.sprite.Sprite.__init__(self)  # 调用pygame.sprite.Sprite的构造方法

        self.image = image  # 防御塔的图片
        self.rect = self.image.get_rect()  # 防御塔的rect
        self.rect.x = pos[0]
        self.rect.y = pos[1]

        self.have_target = False  # 标识防御塔当前是否又攻击对象（实时变化，这里是默认值）
        self.angle = 0  # 防御团发射弹药的角度（实时变化，这里是默认值）
        self.last_shoot_time = pygame.time.get_ticks()  # 上次发射弹药的时间
        self.shoot_cooldown = 2000  # 防御塔发射弹药的冷却时间

    # 该函数为每轮游戏循环中防御塔需要做的事情
    def update(self, enemy_group: pygame.sprite.Group):
        self.have_target = False  # 初始情况下没有攻击目标

        for enemy in enemy_group:  # 遍历敌人组中的每一个敌人
            if enemy.is_living == True:  # 只要敌人还活着，就把它作为攻击目标
                target_x, target_y = enemy.rect.midleft  # 攻击目标为敌人的左侧中间位置
                self.have_target = True  # 找到目标了，标志位设为True
                break  # 找到一个敌人后，就不再寻找新目标

        if self.have_target:  # 找到目标后，通过计算坐标距离，确定子弹发射的角度
            x_dist = target_x - self.rect.midleft[0]
            y_dist = -(target_y - self.rect.midleft[1] - 20)
            self.angle = math.degrees(math.atan2(y_dist, x_dist))  # 计算发射角度

            # 每隔shoot_cooldown时间间隔，向目标发射雪球
            if pygame.time.get_ticks() - self.last_shoot_time > self.shoot_cooldown:  # 如果雪球冷却完毕，即可以发射新雪球了
                self.last_shoot_time = pygame.time.get_ticks()  # 更新最新发射时间
                snowball = Snowball(snowball_image, (self.rect.midleft[0], self.rect.midleft[1] - 20),
                                    SNOWBALL_INITIAL_SPEED, self.angle, SNOWBALL_INITIAL_POWER)  # 新生成一个雪球对象
                snowball_group.add(snowball)  # 把发射的雪球加入雪球组里


# 雪球类：图片、(初始横坐标、初始纵坐标)、移动速度、发射角度
class Snowball(pygame.sprite.Sprite):
    def __init__(self, image: pygame.image, initial_pos: tuple, speed: int, angle: math.degrees, power: int):
        pygame.sprite.Sprite.__init__(self)

        self.init_x = initial_pos[0]  # 该雪球发射位置的横坐标
        self.init_y = initial_pos[1]  # 该雪球发射位置的纵坐标
        self.shoot_time = pygame.time.get_ticks()  # 该雪球的发射时间

        self.image = image  # 该雪球的图像
        self.rect = self.image.get_rect()  # 雪球的rect
        self.rect.centerx = initial_pos[0]  # 雪球rect的初始中央位置即为发射坐标
        self.rect.centery = initial_pos[1]  # 雪球rect的初始中央位置即为发射坐标

        self.speed = speed  # 该雪球的发射速度
        self.power = power  # 该雪球的攻击力
        self.angle = math.radians(angle)  # 该雪球的发射角度

    def update(self, screen):
        # 检测雪球是否出画
        if self.rect.right < 0 or self.rect.left > SCREEN_WIDTH or self.rect.bottom < 0 or self.rect.top > SCREEN_HEIGHT:
            self.kill()  # 如果出画，则回收该雪球

        # 移动雪球
        self.rect.centerx = self.init_x + (pygame.time.get_ticks() - self.shoot_time) * self.speed * math.cos(
            self.angle)
        self.rect.centery = self.init_y + (pygame.time.get_ticks() - self.shoot_time) * self.speed * (
            -(math.sin(self.angle)))

    # pygame.draw.rect(screen, (0,0,0), self.rect, 1) # 显示雪球的rect区域（测试时用）


# 雪弹：图片、(初始横坐标、初始纵坐标)、移动速度、发射角度
class Snowbomb(pygame.sprite.Sprite):
    def __init__(self, image: pygame.image, initial_pos: tuple, speed: int, angle: math.degrees, power: int,
                 dest_pos: tuple, bomb_images: list, castle: Castle) -> None:

        global SNOWBOMB_COST  # 标识SNOWBOMB_COST为全局的常量，而非在函数体内新定义的

        pygame.sprite.Sprite.__init__(self)  # 调用父类的构造函数

        castle.money -= SNOWBOMB_COST  # 给城堡减损金钱值

        self.dest_pos = dest_pos  # 目的坐标（爆炸点）的位置
        self.init_x = initial_pos[0]  # 初始横坐标
        self.init_y = initial_pos[1]  # 初始纵坐标
        self.bomb_images = bomb_images  # 雪弹爆炸后的图片（共六张）
        self.image = image  # 雪弹图片
        self.rect = self.image.get_rect()  # 雪弹的rect
        self.rect.centerx = initial_pos[0]  # rect初始中心点横坐标即为初始横坐标
        self.rect.centery = initial_pos[1]  # rect初始中心点总坐标即为初始纵坐标
        self.centerrect = pygame.Rect(initial_pos[0], initial_pos[1], 20, 20)
        self.shoot_time = pygame.time.get_ticks()  # 雪弹发射时的时间

        self.speed = speed  # 雪弹的速度
        self.power = power  # 雪弹的攻击力
        self.angle = math.radians(angle)  # 雪弹的发射角度
        self.bombed = False  # 是否已爆炸
        self.last_substate_time = pygame.time.get_ticks()  # 上次切换图片帧的时间
        self.substate_cooldown = 100  # 切换图片帧的冷却时间（间隔时间）
        self.substate = 0  # 当前所处图片帧的下标

    def update(self, screen: pygame.Surface, enemy_group: pygame.sprite.Group) -> None:
        # 雪弹是否出画
        if self.rect.right < 0 or self.rect.left > SCREEN_WIDTH or self.rect.bottom < 0 or self.rect.top > SCREEN_HEIGHT:
            self.kill()  # 出画则回收该雪弹

        # 如果当前雪弹尚没有爆炸，则应该继续移动， 横纵方向分别移动
        if self.bombed == False:
            self.rect.centerx = self.init_x + (pygame.time.get_ticks() - self.shoot_time) * self.speed * math.cos(
                self.angle)
            self.rect.centery = self.init_y + (pygame.time.get_ticks() - self.shoot_time) * self.speed * (
                -(math.sin(self.angle)))
            self.centerrect.centerx = self.init_x + (pygame.time.get_ticks() - self.shoot_time) * self.speed * math.cos(
                self.angle)
            self.centerrect.centery = self.init_y + (pygame.time.get_ticks() - self.shoot_time) * self.speed * (
                -(math.sin(self.angle)))

        # pygame.draw.rect(screen, (0, 0, 0), self.rect, 1) # 显示rect的位置，调试用
        # pygame.draw.rect(screen, (0, 0, 0), self.centerrect, 1) # 显示rect的位置，调试用

        if self.centerrect.centerx >= self.dest_pos[0]:  # 只要雪弹中心点的横坐标大于等于目的地的横坐标，就认为它现在可以爆炸了
            if self.bombed == False:  # 如果还没有爆炸
                # print("击中啦")
                self.bombed = True  # 则设置成爆炸状态

                # 下面四行对rect的操作都是为了让rect在保持原来的中心点的基础上，宽和高增大到原来的两倍
                self.rect.x -= self.rect.width / 2  # 调节rect的横坐标到合适的位置
                self.rect.y -= self.rect.height / 2  # 调节rect的纵坐标到合适的位置
                self.rect.width *= 2  # rect宽度*2
                self.rect.height *= 2  # rect高度*2

                self.image = self.bomb_images[0]  # 设置雪弹的图片爆炸态的第0张图片
                self.check_targeted(enemy_group)  # 遍历敌人，检测是否击中了敌人
            else:  # 如果已经处于爆炸态了
                if self.substate < len(self.bomb_images) - 1:  # 如果还没有显示到爆炸态最后一张照片
                    self.update_substate()  # 则调用更新图片帧的方法
                else:  # 如果已经显示到爆炸态最后一张照片
                    self.kill()  # 回收该雪弹

    # 当雪弹处于爆炸态后，会调用该函数，该函数将一帧帧更新爆炸态的图片来模拟动画效果
    def update_substate(self) -> None:
        if pygame.time.get_ticks() - self.last_substate_time > self.substate_cooldown:  # 如果爆炸太图片帧更新冷却时间结束
            self.last_substate_time = pygame.time.get_ticks()  # 更新上次更新帧的时间为当前时间
            self.substate += 1  # 增加图篇帧下标
            if self.substate < len(self.bomb_images):  # 防止下标越界
                self.image = self.bomb_images[self.substate]  # 把图像更新到指定的爆炸态下标对应的图像

    # 检查有没有击中敌人
    def check_targeted(self, enemy_group: pygame.sprite.Group) -> None:
        targeted_enemies = pygame.sprite.spritecollide(self, enemy_group, False)  # 该函数会返回命中的敌人
        for enemy in targeted_enemies:  # 遍历每一个被命中的敌人
            enemy.health -= self.power  # 把每个命中的敌人的健康值减去该雪弹的攻击力


# 僵尸类
class Zombie(pygame.sprite.Sprite):
    def __init__(self, images, initial_pos, initial_health, initial_speed):
        '''
        僵尸类
        :param images: 该种类僵尸的所有图片（里面包含一个个子列表）
        :param initial_pos: 僵尸出现的位置
        :param initial_health: 僵尸的初始健康值和最大健康值
        :param initial_speed: 僵尸的移动速度
        '''
        pygame.sprite.Sprite.__init__(self)  # 调用父类的构造方法

        self.images = images  # 该种类僵尸的所有图片
        self.state = 0  # 0: normal; 1: attacking; 2: die
        self.substate = 0  # 某一state的具体图片的编号
        self.image = self.images[self.state][self.substate]  # 初始图片应为正常态的第0张照片
        self.rect = pygame.Rect(0, 0, 25, 70)  # 获取rect
        self.rect.center = initial_pos  # 设置rect的位置，使其在击中检测这方面更加合理

        self.is_living = True  # 是否活着
        self.max_health = initial_health  # 僵尸的最大健康值
        self.health = self.max_health  # 僵尸的初始健康值
        self.speed = initial_speed  # 僵尸的移动速度
        self.last_substate_time = pygame.time.get_ticks()  # 上次切换当前状态图片帧的时间
        self.last_attack_time = pygame.time.get_ticks()  # 上次对城堡进行攻击的时间
        self.last_state_time = pygame.time.get_ticks()  # 上次切换状态的时间
        self.last_move_time = pygame.time.get_ticks()  # 上次移动的时间
        self.attack_cooldown = 1000  # 攻击冷却时间
        self.substate_cooldown = 50  # 切换图片帧的冷却时间
        self.state_cooldown = 1000  # 切换状态的冷却时间
        self.move_cooldown = 300  # 移动的冷却时间

        # 血条的相关属性
        self.fullbloodrect = pygame.Rect(self.rect.left + 15, self.rect.bottom + 15, 25, 8)  # 满血的rect
        self.fullbloodimage = scale_image(pygame.image.load('img/bloodline/hese.jfif'), 0.04, 0.013)  # 满血的图像
        self.currentbloodimage = scale_image(pygame.image.load('img/bloodline/yello.webp'), 1, 0.02)  # 当前血量的图像

    def update(self, surface, castle: Castle, zombie_group: pygame.sprite.Group):  # 在这里zombie_group没有用上，主要是为了迁就UFO类
        if self.is_living:  # 存活标志位为真时

            # 如果当前到达了城堡且此前的状态是正常态，就把状态改为攻击态
            if self.rect.left <= castle.rect.right and self.state == 0:
                self.update_state(1)

            # 更新僵尸的位置
            self.update_pos()

            # 如果当前处于攻击态，则对城堡发动攻击
            if self.state == 1:
                if pygame.time.get_ticks() - self.last_attack_time > self.attack_cooldown:  # 如果攻击冷却时间过了
                    castle.health -= 25  # 城堡的健康值减少
                    if castle.health < 0:  # 如果城堡的健康值小于0
                        castle.health = 0  # 则将其设置为0
                    self.last_attack_time = pygame.time.get_ticks()  # 更新上次攻击时间

            # 检查自己的健康值是否为0
            if self.health <= 0:  # 如果为0
                castle.money += 120  # 则城堡的金钱值加120
                castle.score += 120  # 城堡的成绩值加120
                self.update_state(2)  # 设置自己的状态为死亡态
                self.is_living = False  # 存活标志位设为False
                self.last_substate_time = pygame.time.get_ticks()  # 更新上次切换图片帧的时间
        else:  # 存活标志位为假时
            if self.substate == len(self.images[2])-1 or pygame.time.get_ticks() - self.last_state_time > self.state_cooldown:
                self.kill()  # 回收该对象

        # 不管当前僵尸是否存活，只要还没有被回收，都需要更新图片帧
        self.update_substate()

        # 显示该僵尸对象
        surface.blit(self.image, (self.rect.x - 28, self.rect.y))
        # pygame.draw.rect(surface, (255,255,255), self.rect, 1) # 显示rect，测试时用

        # 显示血条
        if self.health > 0: # 健康值大于0才显示血条
            surface.blit(self.fullbloodimage, self.fullbloodrect) # 画出满血血条
            self.scale_x = (self.fullbloodimage.get_width() * (self.health / self.max_health)) / (
                self.currentbloodimage.get_width()) # 计算当前血量血条的比例值
            surface.blit(scale_image(self.currentbloodimage, self.scale_x, 1), self.fullbloodrect) # 画出当前血条
            pygame.draw.rect(surface, (0, 0, 1), self.fullbloodrect, 1) # 画出满血血条的黑框

    def update_substate(self):  # 更新僵尸当前状态的图片帧
        self.image = self.images[self.state][self.substate]  # 显示当前的图片帧
        if pygame.time.get_ticks() - self.last_substate_time > self.substate_cooldown:  # 如果切换图片帧的冷却时间过了
            self.last_substate_time = pygame.time.get_ticks()  # 更新上次切换图片帧的时间
            self.substate = (self.substate + 1) % len(self.images[self.state])  # 更新图片帧

    def update_state(self, state_num):  # 更新僵尸的状态
        if state_num != self.state:  # 如果被更新成的状态不是当前的状态，才需要更新
            self.state = state_num  # 更新状态
            self.substate = 0  # 到达新更新的状态后，应显示第一个图像帧
            self.last_state_time = pygame.time.get_ticks()  # 更新上次切换状态的时间为当前时间按
            self.last_substate_time = pygame.time.get_ticks()  # 更新上次切换图片帧的时间

    def update_pos(self):  # 更逊僵尸的位置
        if self.state == 0:  # 当处于normal状态时，zombie要不断逼城堡发动进攻
            self.rect.x -= self.speed  # 只涉及横坐标的移动
            self.fullbloodrect.x -= self.speed  # 血条的位置要跟着移动，始终在僵尸的下方显示


class UFO(pygame.sprite.Sprite):
    def __init__(self, images, initial_pos, initial_health, initial_speed):
        pygame.sprite.Sprite.__init__(self) # 调用父类的构造方法

        self.images = images # 该UFO对象的图集
        self.state = 0  # 0: normal; 1: die to left ; 2: die to right
        self.substate = 0  # 某一状态的图片帧的编号
        self.image = self.images[self.state][self.substate] # 初始图片应当是状态0的图片帧0
        self.rect = self.image.get_rect() # ufo的rect
        self.rect.center = initial_pos # 设置rect的初始位置

        self.is_living = True # 是否存活
        self.max_health = initial_health # 最大健康值
        self.health = self.max_health # 实际健康值，初始时是满的
        self.initial_speed = initial_speed # 初始速度
        self.speed = random.randint(initial_speed, initial_speed+8) # 实际速度
        self.last_substate_time = pygame.time.get_ticks() # 上次更相信图片帧的时间
        self.last_attack_time = pygame.time.get_ticks() # 上次攻击的时间
        self.last_state_time = pygame.time.get_ticks()  # 指上次切换坠毁图片的时间
        self.last_move_time = pygame.time.get_ticks() # 上次移动的时间
        self.last_changedir_time = pygame.time.get_ticks() # 上次改变方向的时间
        self.attack_cooldown = 5000 # 攻击冷却时间
        self.substate_cooldown = 50 # 切换图片帧冷却时间
        self.state_cooldown = 5000 # 切换状态冷却时间
        self.move_cooldown = 300 # 移动冷却时间
        self.changedir_cooldown = 3000 # 改变方向冷却时间
        self.direction_flag = random.choice((-1, 1)) # 方向标志位

        # 血条的相关属性
        self.fullbloodrect = pygame.Rect(self.rect.left + 10, self.rect.bottom + 15, 61, 8) # 满血rect
        self.fullbloodimage = scale_image(pygame.image.load('img/bloodline/hese.jfif'), 0.1, 0.013) # 满血图像
        self.currentbloodimage = scale_image(pygame.image.load('img/bloodline/yello.webp'), 1, 0.02) # 实际血量图像

    def update(self, surface, castle, enemy_group):
        if self.is_living: # 当存活时

            self.update_pos() # 更新位置
            self.attack() # 发动攻击

            # 检查自己的健康值是否降低到了0或者以下
            if self.health <= 0:
                castle.money += 300 # 城堡的金钱增加
                castle.score += 300 # 城堡的成绩增加
                if self.direction_flag == -1: # 如果死亡之前是在向左移动
                    self.update_state(1)  # 进入向左坠毁状态
                else:
                    self.update_state(2)  # 进入向右坠毁状态
                self.is_living = False # 存活标志位改为假
                self.crash_time = pygame.time.get_ticks() # 设置坠机事件为当前时间
                self.crash_x = self.rect.centerx # 设置坠机位置
                self.crash_y = self.rect.centery # 设置坠机位置


        else:  # is_living = False
            if self.substate < len(self.images[self.state]) - 1: # 如果图片帧切换冷却时间已过
                self.update_substate() # 更新图片帧

            # 执行空难方法
            self.air_crash()

            # 砸到别人与移除自己
            self.check_collision(castle, enemy_group)

        # 不管是否存活，都要把ufo显示在屏幕上
        surface.blit(self.image, (self.rect.x, self.rect.y))
        # pygame.draw.rect(surface, (255,255,255), self.rect, 1)

        # 健康值大于0时，显示ufo的血条
        if self.health > 0:
            surface.blit(self.fullbloodimage, self.fullbloodrect)
            self.scale_x = (self.fullbloodimage.get_width() * (self.health / self.max_health)) / (
                self.currentbloodimage.get_width())
            surface.blit(scale_image(self.currentbloodimage, self.scale_x, 1), self.fullbloodrect)
            pygame.draw.rect(surface, (0, 0, 1), self.fullbloodrect, 1)

    def update_substate(self):
        self.image = self.images[self.state][self.substate]
        if pygame.time.get_ticks() - self.last_substate_time > self.substate_cooldown:
            self.last_substate_time = pygame.time.get_ticks()
            self.substate += 1

    def update_state(self, state_num):
        if state_num != self.state:
            self.state = state_num
            self.substate = 0
            self.last_state_time = pygame.time.get_ticks()
            self.last_substate_time = pygame.time.get_ticks()

    def attack(self):
        x_dist = 280 - self.rect.midleft[0]
        y_dist = -(430 - self.rect.midleft[1] - 20)
        self.angle = math.degrees(math.atan2(y_dist, x_dist))


        if pygame.time.get_ticks() - self.last_attack_time > self.attack_cooldown:
            self.last_attack_time = pygame.time.get_ticks()
            snowball = Snowball(grenade_image, (self.rect.midleft[0], self.rect.midleft[1] - 20),
                                SNOWBALL_INITIAL_SPEED, self.angle, SNOWBALL_INITIAL_POWER)
            grenade_group.add(snowball)

    def update_pos(self):
        # 当处于normal状态时,ufo在屏幕上空随即移动
        if self.state == 0:
            if pygame.time.get_ticks() - self.last_changedir_time > self.changedir_cooldown:
                self.direction_flag = -self.direction_flag
                self.last_changedir_time = pygame.time.get_ticks()
                self.changedir_cooldown = random.randint(2000, 10000)
                self.speed = random.randint(self.initial_speed, self.initial_speed + 10)

            if self.rect.left < 0:
                self.rect.left = 0
                self.direction_flag = -self.direction_flag
                self.last_changedir_time = pygame.time.get_ticks()
                self.changedir_cooldown = random.randint(2000, 10000)
                self.speed = random.randint(self.initial_speed, self.initial_speed + 8)

            if self.rect.right > 1136:
                self.rect.right = 1136
                self.direction_flag = -self.direction_flag
                self.last_changedir_time = pygame.time.get_ticks()
                self.changedir_cooldown = random.randint(2000, 10000)
                self.speed = random.randint(self.initial_speed, self.initial_speed + 10)

            self.rect.x += self.speed * self.direction_flag
            self.fullbloodrect.x += self.speed * self.direction_flag

    def air_crash(self):
        # 本方法用于在UFO坠毁过程中更新UFO的位置
        self.rect.centery = self.crash_y + (pygame.time.get_ticks() - self.crash_time) * self.speed * (
            -math.sin(60))
        if self.direction_flag == -1:
            self.rect.centerx = self.crash_x - 0.01 * (
                    pygame.time.get_ticks() - self.crash_time) * self.initial_speed * math.cos(60)
        else:
            self.rect.centerx = self.crash_x + 0.01 * (
                    pygame.time.get_ticks() - self.crash_time) * self.initial_speed * math.cos(60)

    # pygame.draw.rect(screen, (0,0,0), self.rect, 1)

    def check_collision(self, castle, enemy_group):
        if self.rect.top > SCREEN_HEIGHT: # 如果出画了
            self.kill() # 则回收该ufo

        if self.rect.colliderect(castle.rect): # 如果坠机过程中砸到了城堡
            castle.health -= 30 # 城堡的健康值减少
            self.kill() # 回收该ufo
        collision_list = pygame.sprite.spritecollide(self, enemy_group, False) # 坠毁过程中是否砸到僵尸
        for zombie in collision_list:
            if not (zombie is self):
                zombie.health -= 30 # 被砸到的僵尸的健康值减少
        if len(collision_list) > 1: # 只要砸到了僵尸
            self.kill() # 就把该ufo回收


# 创建城堡对象
castle = Castle(castle_image, (CASTLE_INITIAL_X, CASTLE_INITIAL_Y), CASTLE_INITIAL_HEALTH, CASTLE_INITIAL_MONEY)

# 创建准星对象
crosshair = Crosshair(crosshair_image)

# 创建按钮对象
repair_button = Button(repair_button_image, (SCREEN_WIDTH - 260, 10)) # 维修按钮
defender_button = Button(defender_buttion_image, (SCREEN_WIDTH - 180, 10)) # 防御塔按钮
maxhealth_button = Button(maxhealth_button_image, (SCREEN_WIDTH - 105, 10)) # 提高最大生命值按钮

# 创建各个精灵组
defender_group = pygame.sprite.Group() # 防御塔组
snowball_group = pygame.sprite.Group() # 雪球组
snowbomb_group = pygame.sprite.Group() # 雪弹组
enemy_group = pygame.sprite.Group() # 敌人组，里面装所有的生成的僵尸和ufo
grenade_group = pygame.sprite.Group() # ufo发射的子弹组

# game loop
run = True
while run:

    clock.tick(FRAMES_PER_SECOND) # 限制刷新率

    if game_over == False:
        screen.blit(bg, (0, 0))

        # 画出城堡
        castle.draw(screen)
        # 鼠标射击
        shoot((SNOWBALL_INITIAL_X, SNOWBALL_INITIAL_Y), SNOWBALL_INITIAL_SPEED)

        # 画出防护塔并执行其动作（如果有的话）
        defender_group.draw(screen)
        defender_group.update(enemy_group)

        # 画出准星
        crosshair.draw(screen)

        # 雪球/雪弹及其相关操作
        snowball_group.update(screen)
        snowball_group.draw(screen)
        snowbomb_group.update(screen, enemy_group)
        snowbomb_group.draw(screen)
        grenade_group.update(screen)
        grenade_group.draw(screen)

        check_snowball_zombie(snowball_group, enemy_group)
        check_grenade_castle(grenade_group, castle)
        enemy_group.update(screen, castle, enemy_group)

        # 展示文字信息
        display_textinfo()

        # 处理按钮事件
        if repair_button.draw(screen):
            if castle.money >= REPAIR_COST and castle.health < castle.max_health:
                castle.health += 500
                castle.money -= REPAIR_COST
                if castle.health > castle.max_health:
                    castle.health = castle.max_health

        if defender_button.draw(screen):
            if castle.money >= DEFENDER_COST and len(defender_group) < MAX_DEFENDER:
                # print("new defender!")
                defender = Defender(
                    defender_image,
                    (DEFENDER_POSITIONS[len(defender_group)][0],
                     DEFENDER_POSITIONS[len(defender_group)][1])
                )
                defender_group.add(defender)
                castle.money -= DEFENDER_COST

        if maxhealth_button.draw(screen):
            if castle.money >= INCREASELIFE_COST:
                castle.max_health += 200
                castle.money -= INCREASELIFE_COST

        #如果当前关卡已产生的ufo小于当前关卡总的ufo的数量，则继续产生新的ufo
        if level_current_ufo < current_level:
            if pygame.time.get_ticks() - last_zombie_time > NEW_ZOMBIE_COOLDOWN:
                # checkpoint - ufo
                ufo = UFO(ufo_all_images, (ZOMBIE_INITIAL_X, 100), ufo_health, ZOMBIE_INITIAL_SPEED)
                enemy_group.add(ufo)
                level_current_ufo += 1
                level_current_enemy+=1

        if level_current_enemy < level_total_enemy:
            if pygame.time.get_ticks() - last_zombie_time > NEW_ZOMBIE_COOLDOWN:
                # 产生新的僵尸
                zombie_index = random.randint(0, len(zombie_types) - 1)
                zombie = Zombie(zombie_all_images[zombie_index],
                                (ZOMBIE_INITIAL_X, random.randint(ZOMBIE_INITIAL_Y[0], ZOMBIE_INITIAL_Y[1])),
                                zombie_health, ZOMBIE_INITIAL_SPEED)
                enemy_group.add(zombie)

                # 更新上次产生僵尸的时间和现有僵尸的个数
                last_zombie_time = pygame.time.get_ticks()
                level_current_enemy += 2

        # 如果当前关卡已产生的敌人数量大于等于该关卡要求的数量，则不再产生新僵尸，并判断该局游戏是否胜利
        if level_current_enemy >= level_total_enemy:
            enemies_living = 0
            for enery in enemy_group:
                if enery.is_living == True:
                    enemies_living += 1

            if enemies_living == 0 and next_level == False:
                next_level = True
                level_update_time = pygame.time.get_ticks()

        # 进行下一个关卡
        if next_level == True:
            draw_text('LEVEL COMPLETE!', font_2, WHITE, (SCREEN_WIDTH / 2 - 100, SCREEN_HEIGHT / 2))

            if castle.score > highest_score:
                highest_score = castle.score
                with open('score.txt', 'w') as file:
                    file.write(str(highest_score))
            if pygame.time.get_ticks() - level_update_time > 2000:
                next_level = False
                current_level += 1
                enemy_group.empty()
                last_zombie_time = pygame.time.get_ticks()
                level_current_enemy = 0
                level_current_ufo = 0
                level_total_enemy = current_level * LEVEL_TOTAL_ENEMY_GETTER
                zombie_health += 5
            # ufo_health += 10

        # 检查堡类的生命值，如果小于零，泽把“游戏结束”标识为真
        if castle.health <= 0:
            game_over = True
            castle.draw(screen)

    else:  # game_over == True
        draw_text('GAME OVER!', font_1, RED, (SCREEN_WIDTH / 2 - 50, SCREEN_HEIGHT / 2 - 30))
        draw_text('PRESS "SPACE" TO PLAY AGAIN!', font_1, RED, (400, 360))
        pygame.mouse.set_visible(True)
        key = pygame.key.get_pressed()
        if key[pygame.K_SPACE]:
            # reset variables
            game_over = False
            level = 1
            last_zombie = pygame.time.get_ticks()
            enemy_group.empty()
            defender_group.empty()
            castle.score = 0
            castle.health = 1000
            castle.max_health = castle.health
            castle.money = CASTLE_INITIAL_MONEY
            zombie_health = 100
            ufo_health = 150
            pygame.mouse.set_visible(False)

    # 监测用户是否希望退出游戏
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False

    pygame.display.update()

pygame.quit()
